<!--**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 *-->
<template>
  <div id="product-images-container">
    <div
      id="product-images-dropzone"
      :class="['dropzone', 'dropzone-container', { full: files.length <= 0 }]"
    >
      <div
        :class="[
          'dz-preview',
          'openfilemanager',
          { 'd-none': loading || files.length <= 0 },
        ]"
      >
        <div>
          <span><i class="material-icons">add_a_photo</i></span>
        </div>
      </div>
      <div
        :class="[
          'dz-default',
          'dz-message',
          'openfilemanager',
          'dz-clickable',
          { 'd-none': loading || files.length > 0 },
        ]"
      >
        <i class="material-icons">add_a_photo</i><br>
        {{ $t('window.dropImages') }}<br>
        <a>{{ $t('window.selectFiles') }}</a><br>
        <small>
          {{ $t('window.recommendedSize') }}<br>
          {{ $t('window.recommendedFormats') }}
        </small>
      </div>

      <div
        class="dropzone-loading"
        v-if="loading"
      >
        <div class="spinner" />
      </div>
    </div>

    <dropzone-window
      class="dropzone-window"
      v-if="selectedFiles.length > 0"
      :selected-files="selectedFiles"
      :dropzone="dropzone"
      @unselectAll="unselectAll"
      @removeSelection="showModal"
      @selectAll="selectAll"
      @saveSelectedFile="saveSelectedFile"
      @replacedFile="manageReplacedFile"
      @openGallery="toggleGallery"
      :files="files"
      :locales="locales"
      :selected-locale="selectedLocale"
      :loading="buttonLoading"
      :is-multi-store-active="isMultiStoreActive"
      :shop-id="shopId"
    />

    <modal
      v-if="isModalShown"
      :confirmation="true"
      :modal-title="
        $tc('modal.title', selectedFiles.length, {
          'filesNb': selectedFiles.length,
        })
      "
      :confirm-label="$t('modal.accept')"
      :cancel-label="$t('modal.close')"
      @confirm="removeSelection"
      @close="hideModal"
    />

    <div class="dz-template d-none">
      <div class="dz-preview dz-file-preview">
        <div class="dz-image">
          <img data-dz-thumbnail>
        </div>
        <div class="dz-progress">
          <span
            class="dz-upload"
            data-dz-uploadprogress
          />
        </div>
        <div class="dz-success-mark">
          <span>✔</span>
        </div>
        <div class="dz-error-mark">
          <span>✘</span>
        </div>
        <div class="dz-error-message">
          <span data-dz-errormessage />
        </div>
        <div class="dz-hover">
          <i class="material-icons drag-indicator">drag_indicator</i>
          <div class="md-checkbox">
            <label>
              <input type="checkbox">
              <i class="md-checkbox-control" />
            </label>
          </div>
        </div>
        <div class="iscover">
          {{ $t('window.cover') }}
        </div>
      </div>
    </div>

    <dropzone-photo-swipe
      :files="selectedFiles"
      @closeGallery="toggleGallery"
      v-if="selectedFiles.length > 0 && galleryOpened"
    />
  </div>
</template>

<script lang="ts">
  import Router from '@components/router';
  import {
    getProductImages,
    saveImageInformations,
    saveImagePosition,
    replaceImage,
    removeProductImage,
  } from '@pages/product/service/image';
  import ProductMap from '@pages/product/product-map';
  import ProductEventMap from '@pages/product/product-event-map';
  import Modal from '@PSVue/components/Modal.vue';
  import {defineComponent} from 'vue';
  import DropzoneWindow from './DropzoneWindow.vue';
  import DropzonePhotoSwipe from './DropzonePhotoSwipe.vue';

  /* eslint-disable camelcase */
  export interface PSDropzoneFile extends Dropzone.DropzoneFile {
    image_id: string;
    is_cover: boolean;
    legends: Record<string, string>;
    shop_ids: number[];
    isAssociatedToCurrentShop: boolean;
  }
  /* eslint-enable camelcase */

  const {$} = window;

  const router = new Router();
  const DropzoneMap = ProductMap.dropzone;
  const DropzoneEvents = ProductEventMap.dropzone;

  interface DropzoneStates {
    dropzone: Dropzone,
    configuration: Dropzone.DropzoneOptions & {params: KeyStringRecord},
    files: Array<PSDropzoneFile>,
    selectedFiles: Array<PSDropzoneFile>,
    translations: Array<PSDropzoneFile>,
    loading: boolean,
    selectedLocale?: Record<string, any>,
    buttonLoading: boolean,
    isModalShown: boolean,
    galleryOpened: boolean,
    sortableContainer: JQuery | null;
  }

  export default defineComponent({
    name: 'Dropzone',
    data(): DropzoneStates {
      return {
        dropzone: <any>null,
        configuration: {
          url: router.generate('admin_products_add_image'),
          clickable: DropzoneMap.configuration.fileManager,
          previewTemplate: undefined,
          thumbnailWidth: 130,
          thumbnailHeight: 130,
          thumbnailMethod: 'crop',
          paramName: '',
          method: '',
          params: {},
        },
        files: [],
        selectedFiles: [],
        translations: [],
        loading: true,
        selectedLocale: undefined,
        buttonLoading: false,
        isModalShown: false,
        galleryOpened: false,
        sortableContainer: null,
      };
    },
    props: {
      productId: {
        type: Number,
        required: true,
      },
      shopId: {
        type: Number,
        required: true,
      },
      isMultiStoreActive: {
        type: Boolean,
        required: true,
      },
      locales: {
        type: Array as () => Array<Record<string, any>>,
        required: true,
      },
      formName: {
        type: String,
        required: true,
      },
      token: {
        type: String,
        required: true,
      },
    },
    components: {
      DropzoneWindow,
      Modal,
      DropzonePhotoSwipe,
    },
    computed: {},
    mounted() {
      this.watchLocaleChanges();
      this.watchResetDropzone();
      this.initProductImages();
    },
    methods: {
      /**
       * Watch locale changes to update the selected one
       */
      watchLocaleChanges(): void {
        this.selectedLocale = this.locales[0];

        window.prestashop.instance.eventEmitter.on(
          DropzoneEvents.languageSelected,
          (event: any) => {
            const {selectedLocale} = event;

            this.locales.forEach((locale) => {
              if (locale.iso_code === selectedLocale) {
                this.selectedLocale = locale;
              }
            });
          },
        );
      },
      watchResetDropzone(): void {
        window.prestashop.instance.eventEmitter.on(
          DropzoneEvents.resetDropzone,
          () => {
            this.resetDropzone();
          },
        );
      },
      /**
       * This methods is used to initialize product images we already have uploaded
       */
      async initProductImages(): Promise<void> {
        try {
          const images = await getProductImages(this.productId, this.shopId);

          this.loading = false;
          this.initDropZone();

          images.forEach((image: Dropzone.DropzoneMockFile) => {
            // If you're editing a product assigned to one shop,
            // but you're logged into the admin using the URL of a different shop (in multishop mode),
            // update the image domain to match the current window location (window.location.origin)
            // to avoid loading issues or CORS blocks in the browser.
            const url = new URL(image.image_url);

            if (url.origin !== window.location.origin) {
              url.protocol = window.location.protocol;
              url.host = window.location.host;
              image.image_url = url.toString();
            }

            this.dropzone?.displayExistingFile(image, image.image_url);
          });
        } catch (error) {
          window.$.growl.error({message: error});
        }
      },
      /**
       * Method to initialize the dropzone, using the configuration's state and adding files
       * we already have in database.
       */
      initDropZone(): void {
        this.configuration.previewTemplate = (<HTMLElement> document.querySelector(
          DropzoneMap.dzTemplate,
        )).innerHTML;
        this.configuration.paramName = `${this.formName}[file]`;
        this.configuration.method = 'POST';
        this.configuration.params = {};
        this.configuration.params[
          `${this.formName}[product_id]`
        ] = this.productId;
        this.configuration.params[`${this.formName}[_token]`] = this.token;

        this.sortableContainer = $('#product-images-dropzone');

        this.dropzone = new window.Dropzone(
          DropzoneMap.dropzoneContainer,
          this.configuration,
        );

        this.sortableContainer.sortable({
          items: DropzoneMap.sortableItems,
          opacity: 0.9,
          containment: 'parent',
          distance: 32,
          tolerance: 'pointer',
          cursorAt: {
            left: 64,
            top: 64,
          },
          cancel: '.disabled',
          stop: (event: Event, ui: Record<string, any>) => {
            // Get new position (-1 because the open file manager is always first)
            const movedPosition = ui.item.index() - 1;
            this.updateImagePosition(ui.item.data('id'), movedPosition);
          },
          start: (event: Event, ui: Record<string, any>) => {
            this.sortableContainer?.find(DropzoneMap.dzPreview)?.css('zIndex', 1);
            ui.item.css('zIndex', 10);
          },
        });

        this.dropzone.on(DropzoneEvents.addedFile, (file: PSDropzoneFile) => {
          // Be aware the file type is not always a PSDropzoneFile.
          // this event is called multiple times and the file type may differ:
          // 1. when file is being uploaded it is some sort of native File type
          // 2. when its being loaded from api it is the PSDropzoneFile type

          file.previewElement.dataset.id = file.image_id;

          // check if image is associated with current shop.
          // If shop_ids is missing, it means the image is being uploaded, therefore it is not PSDropzoneFile type,
          // so it will be associated to the shop by default
          if (!file.shop_ids) {
            file.shop_ids = [this.shopId];
          }

          file.isAssociatedToCurrentShop = file.shop_ids.includes(this.shopId);

          if (!file.isAssociatedToCurrentShop) {
            file.previewElement.classList.add('not-associated');
            file.previewElement.dataset.toggle = 'pstooltip';
            file.previewElement.dataset.originalTitle = this.$t('window.notAssociatedToShop');
          } else if (file.is_cover) {
            file.previewElement.classList.add('is-cover');
          }

          file.previewElement.addEventListener('click', () => {
            const input = <HTMLInputElement>file.previewElement.querySelector(DropzoneMap.checkbox);
            input.checked = !input.checked;

            if (input.checked) {
              if (!this.selectedFiles.includes(file)) {
                this.selectedFiles.push(file);
                file.previewElement.classList.toggle('selected');
              }
            } else {
              this.selectedFiles = this.selectedFiles.filter((e) => e.image_id !== file.image_id);
              file.previewElement.classList.toggle('selected');
            }
          });
          this.files.push(file);
        });

        this.dropzone.on(DropzoneEvents.error, (fileWithError: PSDropzoneFile, message: OptionsObject) => {
          $.growl.error({message: message.error});
          this.dropzone.removeFile(fileWithError);
        });

        this.dropzone.on(DropzoneEvents.success, (file: Record<string, any>, response: OptionsObject) => {
          // Append the data required for a product image
          file.image_id = response.image_id;
          file.is_cover = response.is_cover;
          file.legends = response.legends;
          // Update dataset so that it can be selected later
          file.previewElement.dataset.id = file.image_id;
          file.isAssociatedToCurrentShop = response.shop_ids.includes(this.shopId);

          if (file.is_cover) {
            file.previewElement.classList.add('is-cover');
          }
        });
      },
      /**
       * Method to select every files by checking checkboxes and add files to the files state
       */
      selectAll(): void {
        this.selectedFiles = this.files;

        this.editCheckboxes(true);
      },
      /**
       * Method to unselect every files by unchecking checkboxes and empty files state
       */
      unselectAll(): void {
        this.editCheckboxes(false);

        this.selectedFiles = [];

        this.removeTooltips();
      },
      /**
       * Method to remove every selected files from the dropzone
       */
      async removeSelection(): Promise<void> {
        let errorMessage = false;
        let isCoverImageRemoved = false;
        const nbFiles = this.selectedFiles.length;

        await Promise.all(
          this.selectedFiles.map(async (file) => {
            try {
              await removeProductImage(file.image_id);
              this.dropzone.removeFile(file);

              this.files = this.files.filter((e) => file !== e);
              this.selectedFiles = this.selectedFiles.filter((e) => file !== e);

              if (file.is_cover) {
                isCoverImageRemoved = true;
              }
            } catch (error: any) {
              errorMessage = error.responseJSON
                ? error.responseJSON.error
                : error;
            }
          }),
        );

        this.removeTooltips();

        if (errorMessage) {
          $.growl.error({message: errorMessage});
        } else {
          $.growl({
            message: this.$t('delete.success', {
              filesNb: nbFiles,
            }),
          });
        }

        if (isCoverImageRemoved) {
          this.resetDropzone();
        }

        this.hideModal();
      },
      /**
       * Method to manage checkboxes of files mainly used on selectAll and unselectAll
       */
      editCheckboxes(checked: boolean): void {
        this.selectedFiles.forEach((file: Record<string, any>) => {
          const input = file.previewElement.querySelector(DropzoneMap.checkbox);
          input.checked = typeof checked !== 'undefined' ? checked : !input.checked;

          file.previewElement.classList.toggle('selected', checked);
        });
      },
      /**
       * We sometime need to remove tooltip because Vue kick the markup of the component
       */
      removeTooltips(): void {
        $(DropzoneMap.shownTooltips).each((i, element) => {
          $(element).remove();
        });
      },
      /**
       * Save selected file
       */
      async saveSelectedFile(
        captionValue: Record<string, any>,
        isCover: boolean,
        shopId: number|null,
      ): Promise<void> {
        if (!this.selectedFiles.length) {
          return;
        }

        this.buttonLoading = true;

        const selectedFile = this.selectedFiles[0];

        selectedFile.is_cover = isCover;
        selectedFile.legends = captionValue;

        try {
          const savedImage = await saveImageInformations(
            selectedFile,
            this.token,
            this.formName,
            shopId,
          );

          const savedImageElement = <HTMLElement> document.querySelector(
            DropzoneMap.savedImageContainer(savedImage.image_id),
          );

          /**
           * If the image was saved as cover, we need to replace the DOM in order to display
           * the correct one.
           */
          if (savedImage.is_cover) {
            if (!savedImageElement.classList.contains('is-cover')) {
              const coverElement = document.querySelector(
                DropzoneMap.coveredPreview,
              );

              if (coverElement) {
                coverElement.classList.remove('is-cover');
              }

              savedImageElement.classList.add('is-cover');

              this.files = this.files.map((file: PSDropzoneFile) => {
                if (file.image_id !== savedImage.image_id && file.is_cover) {
                  file.is_cover = false;
                }

                return file;
              });
            }
          }
          $.growl({message: this.$t('window.settingsUpdated')});
          this.buttonLoading = false;
        } catch (error: any) {
          $.growl.error({message: error.error});
          this.buttonLoading = false;
        }
      },
      /**
       * Used to save and manage some datas from a replaced file
       */
      async manageReplacedFile(event: Event): Promise<void> {
        const selectedFile = this.selectedFiles[0];
        const eventTarget = <HTMLInputElement> event.target;
        this.buttonLoading = true;

        try {
          if (eventTarget.files && eventTarget.files[0]) {
            const newImage = await replaceImage(
              selectedFile,
              eventTarget.files[0],
              this.formName,
              this.token,
            );
            const imageElement = <HTMLImageElement> document.querySelector(
              DropzoneMap.savedImage(newImage.image_id),
            );
            const imageUpdateTime = new Date();
            imageElement.src = `${newImage.image_url}?${imageUpdateTime.getTime()}`;

            $.growl({message: this.$t('window.imageReplaced')});
            this.buttonLoading = false;
          }
        } catch (error: any) {
          $.growl.error({message: error.responseJSON.error});
          this.buttonLoading = false;
        }
      },
      async updateImagePosition(productImageId: number, newPosition: number): Promise<void> {
        const image = this.files.filter((file) => Number(file.image_id) === productImageId)[0];

        try {
          await saveImagePosition(
            productImageId,
            newPosition,
            this.formName,
            this.token,
            image.isAssociatedToCurrentShop ? this.shopId : null,
          );
        } catch (error: any) {
          this.sortableContainer?.sortable('cancel');
          $.growl.error({message: error.responseJSON.error});
        }
      },
      /**
       * Mainly used when we wants to reset the whole list
       * to reset cover image for example on remove
       */
      resetDropzone(): void {
        this.loading = true;
        this.files.forEach((file) => {
          this.dropzone.removeFile(file);
        });
        this.dropzone.destroy();
        this.dropzone = <any>null;
        this.initProductImages();
      },
      showModal(): void {
        this.isModalShown = true;
      },
      hideModal(): void {
        this.isModalShown = false;
      },
      /**
       * Method used to open the photoswipe gallery
       */
      toggleGallery(): void {
        this.galleryOpened = !this.galleryOpened;
      },
    },
  });
</script>

<style lang="scss" type="text/scss">
@import '~@scss/config/_settings.scss';
@import '~@scss/config/_bootstrap.scss';

.product-page #product-images-dropzone {
  &.full {
    cursor: pointer;
    width: 100%;
  }

  .dropzone-loading {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--#{$cdk}size-160);
  }

  &.dropzone-container {
    .dz-preview {
      position: relative;
      cursor: pointer;

      .iscover {
        display: none;
        left: calc(-1 * var(--#{$cdk}size-2));
        bottom: calc(-1 * var(--#{$cdk}size-2));
        width: calc(100% + var(--#{$cdk}size-4));
        padding: var(--#{$cdk}size-8) var(--#{$cdk}size-16);
      }
      &.is-cover {
        .iscover {
          display: block;
        }
      }

      &:not(.openfilemanager) {
        border: var(--#{$cdk}size-3) solid transparent;

        &:hover {
          border: var(--#{$cdk}size-3) solid var(--#{$cdk}primary-800);
        }

        .dz-image {
          border: 1px solid var(--#{$cdk}primary-300);
          width: 130px;
          height: 130px;
          margin: calc(-1 * var(--#{$cdk}size-3));
        }
      }

      &.openfilemanager {
        border-style: dashed;
        min-width: 130px;

        &:hover {
          border-style: solid;
        }

        > div {
          border: none;

          i {
            font-size: var(--#{$cdk}size-40);
          }
        }
      }

      img {
        margin: 0;
      }

      &:hover {
        .dz-hover {
          background-color: rgba(map-get($map: $cdk-common, $key: "black"), 0.7);

          .drag-indicator,
          .md-checkbox {
            opacity: 1;
          }
        }
      }

      &.selected {
        .md-checkbox {
          opacity: 1;
        }
      }
    }

    .dz-hover {
      position: absolute;
      top: calc(-1 * var(--#{$cdk}size-2));
      left: calc(-1 * var(--#{$cdk}size-2));
      width: calc(100% + var(--#{$cdk}size-4));
      height: calc(100% + var(--#{$cdk}size-4));
      background-color: rgba(map-get($map: $cdk-common, $key: "black"), 0);
      transition: 0.25s ease-out;
      pointer-events: none;
      z-index: 11;

      .drag-indicator {
        position: absolute;
        top: var(--#{$cdk}size-8);
        left: var(--#{$cdk}size-8);
        color: var(--#{$cdk}white);
        opacity: 0;
        transition: 0.25s ease-out;
      }

      .md-checkbox {
        position: absolute;
        bottom: var(--#{$cdk}size-8);
        left: var(--#{$cdk}size-8);
        opacity: 0;
        transition: 0.25s ease-out;

        .md-checkbox-control::before {
          background: transparent;
        }

        input:checked + .md-checkbox-control::before {
          background: var(--#{$cdk}primary-800);
        }
      }
    }

    .dz-preview.not-associated {
      filter: grayscale(0.8);
      opacity: 0.6;
    }
  }
}

.product-page #product-images-container {
  border-radius: var(--#{$cdk}size-4);

  @include media-breakpoint-down(xs) {
    flex-wrap: wrap;
  }

  #product-images-dropzone.dropzone {
    border-radius: var(--#{$cdk}size-4);

    @include media-breakpoint-down(xs) {
      flex-wrap: wrap;
      justify-content: space-around;
      width: 100%;

      .dz-preview {
        width: 100px;
        height: 100px;
        min-height: 100px;
        margin: var(--#{$cdk}size-8);

        &.openfilemanager {
          min-width: 100px;
        }

        img {
          max-width: 100%;
          max-height: 100%;
        }

        .dz-image {
          width: 100px;
          height: 100px;
        }
      }
    }
  }
}
</style>
